package de.mmis.utilities.genericPublisher;

import java.awt.Component;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.lang.reflect.InvocationTargetException;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

import org.apache.log4j.Level;
import org.apache.log4j.Logger;

import de.mmis.core.base.ObjectDescriptor;
import de.mmis.core.base.Pair;
import de.mmis.core.base.Position;
import de.mmis.core.base.Quittable;
import de.mmis.core.base.abstracttree.Tree;
import de.mmis.core.base.abstracttree.TreeParserException;
import de.mmis.core.base.abstracttree.serializer.DeserializableException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.MethodInvocationException;
import de.mmis.core.base.abstracttree.serializer.SerializerHelper.NoMatchingMethodFoundException;
import de.mmis.core.base.device.StaticDevice;
import de.mmis.core.base.event.Event;
import de.mmis.core.base.event.Observable;
import de.mmis.core.base.event.Observer;
import de.mmis.core.base.gclass.GClass;
import de.mmis.core.base.gclass.GParameterized;
import de.mmis.core.base.gui.DeviceControlComponent;
import de.mmis.core.base.gui.DeviceRepresentationComponent;
import de.mmis.core.base.gui.GUIComponent;
import de.mmis.core.base.infrastructure.InfrastructureException;
import de.mmis.core.base.infrastructure.PublishedDeviceDescription.PublishType;
import de.mmis.core.pojop.NetworkPublisher.ServerType;
import de.mmis.core.publishsubscribe.filter.Filter;
import de.mmis.core.sexpression.SExpressionParser;
import de.mmis.utilities.consoleWrapper.ConsoleWrapper;
import de.mmis.utilities.genericPublisher.GenericPublisher.AddDeviceInfo;

/**
 * provides a command line interface for the {@link GenericPublisher}
 * 
 * @author Martin
 * 
 */
public class GenericPublisherMain {

	private static void printHelp() {
		System.out
				.println("usage: java -cp GenericPublisher-full.jar[:object-lib.jar] "
						+ GenericPublisher.class.getName() + " [Options]");

		System.out
				.println("All long options can also be read from a file. In this case, the -- must be omitted.");
		System.out
				.println("Options in the file must be seperated by newlines or ;");
		System.out
				.println("For example the two arguments '--port 1234' can be written as 'port 1234;' in the file");
		System.out
				.println("If parameters for options contain spaces, the space must be escaped using '\\ ' or option must be enclosed in \", which may be escaped using \\\"");
		System.out.println("e.g. 'id \"My Obj\";'");

		// ### General

		System.out.println("\n\nGeneral options:");
		System.out
				.println("--file URL             Read more options from the URL.");
		System.out
				.println("-O | --option STRING   Parse option string as if it were read from a file");
		System.out
				.println("--no-control           Don't try to read commands (q, i) from the commandline");

		// ### Publish

		System.out.println("\n\nPublishing options:");
		System.out
				.println("--object [CLASS] ID SEXP  creates the object with the given ID. Many subsequent options will use this object, otherwise a new --object is used.");
		System.out
				.println("                          @HOST inside the ID is replaced by the Hostname. In following object expressions, you can use @<ID> to refer to this object");
		System.out.println("--tuplespace [HOST PORT]  Use TuplespacePublisher");
		System.out
				.println("--socket [PORT] [BOOL]    Use NetworkPublisher and publish as Socket");
		System.out
				.println("--http [PORT] [BOOL]      Use NetworkPublisher and publish as HTTP");
		System.out
				.println("--telnet [PORT] [BOOL]    Use NetworkPublisher and publish as Telnet");
		System.out
				.println("--ports                   Same as --socket --telnet --http");
		System.out
				.println("--pubsub                  Publish the Observable in the Publish/Subscribe system");
		System.out
				.println("-b | --bind IP            Bind Infrastructure to this IP");
		System.out
				.println("--no-infrastructure       Don't publish the object via the Infrastructure");
		System.out
				.println("--shutdown-method METHOD  Call this method on the last object when exiting");

		System.out
				.println("If you don't set port for --socket/http/telnet, a random port is used.\n"
						+ "The boolean option sets implicit (true) or explicit (false) serialization of the publisher");
		System.out
				.println("If you don't set port and host for --tuplespace, then a Tuplespace will be searched using the infrastructure");

		// ### Tuplespace

		System.out.println("\n\nTuplespace specific options:");
		System.out
				.println("-D | --no-device-info          Don't write additional device information to Tuplespace - must be set for every object");
		System.out
				.println("--history LEASE                Set the command history queue lease time to the specified value in seconds");
		System.out
				.println("-H | --no-history              Don't add commands to the history queue");

		// ### Middleware

		System.out.println("\n\nMiddleware options");
		System.out
				.println("the subscribe methods register for events in the Publish/Subscribe system of the given event class that match the filter and notify the object");
		System.out
				.println("--subscribe FILTER [CLASS]              The object must be Observable<? super CLASS>. If CLASS is not given, it's inferred from the Observer");
		System.out
				.println("--subscribeM FILTER [CLASS] METHODNAME  The method must have signature void (CLASS event). If CLASS is not given, it's inferred from the method");
		System.out
				.println("--subscribeC FILTER [CLASS] METHODCALL  The method must be an S-Expression (methodname arg1 arg2 ...). You can use @EVENT_field to acces field of the event and @EVENT for the whole event.");
		System.out
				.println("--addDevice CLASS ID METHODCALL [TYPE]  Searches in the infrastructure for a device, for each (new) creates a proxy and calls the method, e.g. (add @PROXY @ID)");
		System.out
				.println("                                        CLASS must be an Interface. If TYPE is either SOCKET or TUPLESPACE, the proxy will be of that type. The ID * matches any ID");
		System.out
				.println("--removeDevice ID METHODCALL [TYPE]     for each removed device of the infrastructure with the ID (or * for every) calls the method, e.g. (remove @ID)");

		// ### Device info

		System.out.println("\n\nadditional device information:");
		System.out
				.println("--name NAME_MAP          All display names of the device in the language (ISO-639-3). E. g. ((\"eng\" \"NAME\") (\"deu\" \"NAME\"))");
		System.out
				.println("--position POSITION_MAP  The position of the device relative to a location. E. g. ((\"SmartLab 218\" (x y z)))");
		System.out
				.println("--property STRING TREE   Add an arbitrary entry to the property map. The first argument is the key, the second the value");
		System.out
				.println("These option should be used before any publishing (socket, tuplespace, …) options");

		// ### GUI

		System.out.println("\n\nGUI options:");
		System.out
				.println("-g | --gui                     Use a GUI (ConsoleWrapper)");
		System.out
				.println("-n | --gui-capacity KBYTES     Limit ConsoleWrapper output");
		System.out
				.println("--gui-title STRING             Set the title of the Window");
		System.out
				.println("--panel STRING [CLASS] S-EXPR  Adds the deserialized component with the given String as the title.");
		System.out
				.println("--control CLASS                Add the DeviceControlComponent of the specified class as a tab");
		System.out
				.println("--representation CLASS         Add the DeviceRepresentationComponent of the specified class as a tab");

		// ### misc

		System.out.println("\n\nmiscellaneous options:");
		System.out
				.println("--in STRING              redirect stdin to the given file");
		System.out
				.println("--out STRING             redirect stdout to the given file");
		System.out
				.println("--err STRING             redirect stderr to the given file");
		System.out
				.println("--log-level [CAT] LEVEL  sets the log level of the categorie (or root categorie if none is given)");
	}

	private static LinkedList<List<String>> parseOptions(String line)
			throws Exception {
		LinkedList<List<String>> result = new LinkedList<List<String>>();

		line = line.trim();
		String parameter = "";
		LinkedList<String> option = new LinkedList<String>();
		boolean inPar = false;

		for (int i = 0; i < line.length(); i++) {
			char c = line.charAt(i);

			if (c == ';' && !inPar) {
				// ; in " " is not handled as a seperator

				while (line.charAt(i + 1) == ' ')
					i++;

				if (!parameter.isEmpty())
					option.add(parameter);

				if (!option.isEmpty()) {
					result.add(option);
					option = new LinkedList<String>();
					parameter = "";
				}
				continue;
			}

			if (c == '"') {
				inPar ^= true;
				continue;
			}

			if (!inPar && c == ' ') {
				if (!parameter.isEmpty())
					option.add(parameter);
				parameter = "";
				while (line.charAt(i + 1) == ' ')
					i++;
				continue;
			}

			if (c == '\\')
				// escaped char
				c = line.charAt(++i);

			parameter += c;
		}

		if (inPar)
			throw new Exception("Missing closing \"");

		if (!parameter.isEmpty())
			option.add(parameter);

		if (!option.isEmpty())
			result.add(option);

		return result;
	}

	private static LinkedList<List<String>> loadOptions(URL url)
			throws Exception {
		LinkedList<List<String>> result = new LinkedList<List<String>>();

		try {
			BufferedReader reader = new BufferedReader(new InputStreamReader(
					url.openStream()));

			String line;
			while ((line = reader.readLine()) != null) {
				while (line.endsWith("\\")) {
					String l = reader.readLine();
					if (l == null)
						throw new Exception(
								"End of stream reached, but end of last line was escaped with \\");
					line += l;
				}

				result.addAll(parseOptions(line));
			}

			return result;
		} catch (IOException e) {
			throw new Exception(e);
		}

	}

	private static LinkedList<Pair<Object, String>> objects = new LinkedList<Pair<Object, String>>();

	private static void printInfo(GenericPublisher publisher) {
		for (Pair<Object, String> pair : objects)
			printInfoCreated(pair.getFirst(), pair.getSecond());

		for (PublishHandle handle : publisher.getHandles())
			System.out.println(handle.prettyPrint());
	}

	private static void printInfoCreated(Object object, String id) {
		System.out.println("\nCreated object");
		System.out.println("\t" + object.toString());
		System.out.println("\tID: " + id);
		System.out.println("\tclass: " + object.getClass().getName());
		System.out.println();
	}

	private static Exception optError(List<String> option, String msg,
			Throwable e) {
		return new Exception("In option " + option + ": " + msg, e);
	}

	private static Exception optError(List<String> option, String msg) {
		return optError(option, msg, null);
	}

	static private final GenericPublisher publisher = new GenericPublisher();

	/**
	 * if you want to publish an object using the {@link GenericPublisher},
	 * create an instance of this class and use the appropriate methods.
	 * 
	 * @param args
	 * @throws Exception
	 * @throws InfrastructureException
	 */
	@SuppressWarnings("unchecked")
	public static void main(String[] args) throws Exception,
			InfrastructureException {
		LinkedList<List<String>> options = new LinkedList<List<String>>();

		{
			HashMap<String, String> parameterMap = new HashMap<String, String>();
			parameterMap.put("-O", "--option");
			parameterMap.put("-o", "--object");
			parameterMap.put("-i", "--id");
			parameterMap.put("-b", "--bind");
			parameterMap.put("-D", "--no-device-info");
			parameterMap.put("-H", "--no-history");
			parameterMap.put("-g", "--gui");
			parameterMap.put("-n", "--gui-capacity");

			int i = 0;
			LinkedList<String> option = new LinkedList<String>();
			while (i < args.length) {
				if (args[i].equalsIgnoreCase("--help")) {
					printHelp();
					return;
				}

				String parameter = args[i++];
				if (parameter.startsWith("-")
						&& parameterMap.containsKey(parameter))
					parameter = parameterMap.get(parameter);

				if (parameter.startsWith("--")) {
					if (!option.isEmpty())
						options.add(option);
					option = new LinkedList<String>();

					parameter = parameter.substring(2);
				}

				option.add(parameter);
			}

			if (!option.isEmpty())
				options.add(option);
		}

		args = null; // avoid accessing args further

		/*
		 * VARIABLE DEFINITIONS
		 * ###################################################################
		 */
		boolean control = true;

		Object object = null;
		String id = null;

		boolean writeDeviceInfo = true;
		boolean useInfrastructure = true;

		LinkedList<GUIComponent<?>> guiComponents = new LinkedList<GUIComponent<?>>();

		ConsoleWrapper consoleWrapper = null;
		String title = "";

		final HashMap<Object, LinkedList<Tree>> shutdownMethods = new HashMap<Object, LinkedList<Tree>>();

		String localhost = "";
		try {
			localhost = InetAddress.getLocalHost().getHostName();
			int index = localhost.indexOf('.');
			if (index > 0)
				localhost = localhost.substring(0, index);
		} catch (UnknownHostException e) {
			// ignore
		}

		/*
		 * PARSE COMMAND LINE ARGUMENTS
		 * ###################################################################
		 */

		int i = 0;
		while (i < options.size()) {
			List<String> option = options.get(i++);
			String opt0 = option.get(0);

			// ### GENERAL

			if (opt0.equals("file")) {
				if (option.size() != 2)
					throw optError(option,
							"file option must have one argument: URL");

				URL url = null;
				try {
					url = new URL(option.get(1));
				} catch (MalformedURLException e) {
					throw optError(option, option.get(1)
							+ " is not a valid URL", e);
				}

				LinkedList<List<String>> newOptions = loadOptions(url);
				options.addAll(i, newOptions);
				continue;
			}

			if (opt0.equals("option")) {
				if (option.size() != 2)
					throw optError(option,
							"option option must have one argument: OPTIONS");
				LinkedList<List<String>> newOptions = parseOptions(option
						.get(1));
				options.addAll(i, newOptions);
				continue;
			}

			if (opt0.equals("no-control")) {
				if (option.size() != 1)
					throw optError(option,
							"no-control option must have no arguments");
				control = false;
				continue;
			}

			// ### PUBLISHING

			if (opt0.equals("object")) {
				if (option.size() != 3 && option.size() != 4)
					throw optError(option,
							"object option must have two or three arguments: [CLASS] ID SEXPRESSION");

				Class<?> objectClass = Object.class;
				int ind = 0;
				if (option.size() == 4) {
					try {
						objectClass = Class.forName(option.get(1));
					} catch (ClassNotFoundException e) {
						throw optError(option, "", e);
					}
					ind = 1;
				}

				id = option.get(ind + 1).replace("@HOST", localhost);

				String exp = option.get(ind + 2);
				Tree objectExp = null;
				try {
					objectExp = SExpressionParser.parseUTF8(exp);
				} catch (TreeParserException e) {
					throw optError(option, "Invalid S-Expression " + exp, e);
				}

				// deserialise Object

				synchronized (publisher.idmap) {
					try {
						object = objectExp.getAs(objectClass, publisher.idmap);
					} catch (DeserializableException e) {
						throw optError(option, "Deserialisation error", e);
					}

					publisher.idmap.put("@" + id.toString(), object);
				}

				publisher.properties.put(id, new HashMap<String, Object>());

				objects.add(new Pair<Object, String>(object, id));
				printInfoCreated(object, id);

				writeDeviceInfo = true;
				useInfrastructure = true;

				title += " - " + id;
				if (consoleWrapper != null)
					consoleWrapper.setTitle(title);

				continue;
			}

			if (opt0.equals("pubsub")) {
				if (option.size() != 1)
					throw optError(option,
							"pubsub option must have no argument");

				if (object == null)
					throw optError(option, "object must be specified first");

				if (!(object instanceof Observable))
					throw optError(option, "object is not observable");

				final Observable<?> prod = (Observable<?>) object;
				final String id_ = id;
				final boolean inf = useInfrastructure;

				new Thread(new Runnable() {
					@Override
					public void run() {
						try {
							PubSubPublishHandle handle = publisher.publishPubSub(
									prod, id_, inf);
							System.out.println(handle.prettyPrint());
						} catch (Exception e) {
							e.printStackTrace();
						}
					}
				}, "Publish " + id + " PubSub").start();

				continue;
			}

			if (opt0.equals("tuplespace")) {
				if (option.size() == 2 || option.size() > 3)
					throw optError(option,
							"tuplespace option can have either no or two arguments HOST PORT");

				if (object == null)
					throw optError(option, "object must be specified first");

				int port = 0;
				String host = null;
				InetAddress address = null;

				if (option.size() == 3) {
					host = option.get(1);
					try {
						port = Integer.parseInt(option.get(2));
						if (port < 0)
							throw optError(option, "Port cannot be negative");
					} catch (NumberFormatException e) {
						throw optError(option, "Please specify a valid port", e);
					}

					try {
						address = InetAddress.getByName(host);
					} catch (UnknownHostException e) {
						throw optError(option, "", e);
					}
				}

				final String a = address == null ? null : address
						.getHostAddress();
				final int p = port;
				final Object o = object;
				final String id_ = id;
				final boolean inf = useInfrastructure;

				new Thread(new Runnable() {
					@Override
					public void run() {
						TuplespacePublishHandle handle;

						try {
							if (a != null)
								handle = publisher.publishTuplespace(o, id_, a,
										p, inf);
							else
								handle = publisher.publishTuplespace(o, id_,
										inf);
						} catch (Exception e) {
							e.printStackTrace();
							return;
						}

						System.out.println(handle.prettyPrint());
					}
				}, "Publish " + id + " Tuplespace "
						+ (address == null ? "" : address + ":" + port))
						.start();

				continue;
			}

			if (opt0.equals("socket") || opt0.equals("http")
					|| opt0.equals("telnet")) {
				if (option.size() > 3)
					throw optError(
							option,
							opt0 // socket, http, telnet
									+ "option can have at most two arguments PORT and/or BOOL");

				if (object == null)
					throw optError(option, "object must be specified first");

				int port = 0;
				boolean implicit = false;

				if (option.size() >= 2) {
					try {
						port = Integer.parseInt(option.get(1));
						if (port < 0)
							throw optError(option, "Port cannot be negative");
					} catch (NumberFormatException e) {
						if (option.get(1).equals("true"))
							implicit = true;
						else if (!option.get(1).equals("false"))
							throw optError(
									option,
									option.get(1)
											+ " is neither a valid port number nor true/false",
									e);
					}
				}

				if (option.size() == 3) {
					try {
						port = Integer.parseInt(option.get(2));
						if (port < 0)
							throw optError(option, "Port cannot be negative");
					} catch (NumberFormatException e) {
						if (option.get(2).equals("true"))
							implicit = true;
						else if (!option.get(2).equals("false"))
							throw optError(
									option,
									option.get(2)
											+ " is neither a valid port number nor true/false",
									e);
					}
				}

				final int p = port;
				final Object o = object;
				final String id_ = id;

				ServerType type = null;
				if (opt0.equals("socket"))
					type = ServerType.Socket;
				else if (opt0.equals("http"))
					type = ServerType.HTTP;
				else if (opt0.equals("telnet"))
					type = ServerType.Telnet;

				publishPOJOP(publisher, p, o, id_, type, useInfrastructure,
						implicit);
				continue;
			}

			if (opt0.equals("ports")) {
				if (option.size() > 1)
					throw optError(option,
							"ports option must have no arguments");

				if (object == null)
					throw optError(option, "object must be specified first");

				for (ServerType type : new ServerType[] { ServerType.Socket,
						ServerType.Telnet, ServerType.HTTP })
					publishPOJOP(publisher, 0, object, id, type,
							useInfrastructure, false);

				continue;
			}

			if (opt0.equals("bind")) {
				if (option.size() != 2)
					throw optError(option,
							"bind option must have one argument ADDRESS");

				try {
					InetAddress bindAddress = InetAddress.getByName(option
							.get(1));
					publisher.setBindAddress(bindAddress);
				} catch (UnknownHostException e) {
					throw optError(option, "Can't create InetAddress of "
							+ option.get(1), e);
				}
				continue;
			}

			if (opt0.equals("no-infrastructure")) {
				if (option.size() != 1)
					throw optError(option,
							"no-infrastructure option must have no arguments");

				useInfrastructure = false;
				continue;
			}

			if (opt0.equals("shutdown-method")) {
				if (option.size() != 2)
					throw optError(option,
							"shutdown-method option must have one argument METHOD-EXPRESSION");

				if (object == null)
					throw optError(option, "object must be specified first");

				Tree method;
				try {
					method = SExpressionParser.parseUTF8(option.get(1));
				} catch (TreeParserException e) {
					throw optError(option, "Invalid S-Expression", e);
				}

				LinkedList<Tree> methods = shutdownMethods.get(object);
				if (methods == null) {
					methods = new LinkedList<Tree>();
					shutdownMethods.put(object, methods);
				}
				methods.add(method);

				continue;
			}

			// ### TUPLESPACE

			if (opt0.equals("no-device-info")) {
				if (option.size() != 1)
					throw optError(option,
							"no-device-info option must have no arguments");

				if (object == null)
					throw optError(option, "object must be specified first");

				writeDeviceInfo = false;
				continue;
			}

			if (opt0.equals("history")) {
				if (option.size() != 2)
					throw optError(option,
							"history option must have one argument LEASE_TIME");

				try {
					int commandHistoryLeaseTime = Integer.parseInt(option
							.get(1));
					publisher
							.setCommandHistoryLeaseTime(commandHistoryLeaseTime);
				} catch (NumberFormatException e) {
					throw optError(option, "Please specify a valid port", e);
				}
				continue;
			}

			if (opt0.equals("no-history")) {
				if (option.size() != 1)
					throw optError(option,
							"no-history option must no arguments");

				publisher.setCommandHistoryLeaseTime(-1);
				continue;
			}

			// ### MIDDLEWARE

			if (opt0.equals("subscribe")) {
				if (option.size() != 2 && option.size() != 3)
					throw optError(option,
							"subscribe option must have 1 or 2 arguments FILTER [CLASS]");

				if (object == null)
					throw optError(option, "object must be specified first");

				if (!(object instanceof Observer))
					throw optError(option, "object is not an Observer");

				Filter filter = null;
				try {
					filter = Filter.parse(option.get(1));
				} catch (IOException e) {
					throw optError(option, "Invalid filter", e);
				}

				Class<?> class1 = null;
				if (option.size() == 3) {
					try {
						class1 = Class.forName(option.get(2));
					} catch (ClassNotFoundException e) {
						throw optError(option,
								"class not found: \"" + option.get(2) + "\"", e);
					}

					if (!Event.class.isAssignableFrom(class1))
						throw optError(option, class1 + " is not an Event");
				}

				publisher.addSubscribeObserver((Observer<?>) object,
						(Class<? extends Event>) class1, filter);
				continue;
			}

			if (opt0.equals("subscribeM")) {
				if (option.size() != 3 && option.size() != 4)
					throw optError(option,
							"subscribeM option must have 2 or 3 arguments FILTER [CLASS] METHOD_NAME");

				if (object == null)
					throw optError(option, "object must be specified first");

				Filter filter = null;
				try {
					filter = Filter.parse(option.get(1));
				} catch (IOException e) {
					throw optError(option, "Invalid filter", e);
				}

				String method;
				Class<?> class1 = null;
				if (option.size() == 4) {
					method = option.get(3);
					try {
						class1 = Class.forName(option.get(2));
					} catch (ClassNotFoundException e) {
						throw optError(option,
								"class not found: \"" + option.get(2) + "\"", e);
					}

					if (!Event.class.isAssignableFrom(class1))
						throw optError(option, class1 + " is not an Event");
				} else
					method = option.get(2);

				publisher.addSubscribeMethod(object,
						(Class<? extends Event>) class1, filter, method);
				continue;
			}

			if (opt0.equals("subscribeC")) {
				if (option.size() != 3 && option.size() != 4)
					throw optError(option,
							"subscribeC option must have 2 or 3 arguments FILTER [CLASS] METHOD-EXPRESSION");

				if (object == null)
					throw optError(option, "object must be specified first");

				Filter filter = null;
				try {
					filter = Filter.parse(option.get(1));
				} catch (IOException e) {
					throw optError(option, "Invalid filter", e);
				}

				String methodCall;
				Class<?> class1 = null;
				if (option.size() == 4) {
					methodCall = option.get(3);
					try {
						class1 = Class.forName(option.get(2));
					} catch (ClassNotFoundException e) {
						throw optError(option,
								"class not found: \"" + option.get(2) + "\"", e);
					}

					if (!Event.class.isAssignableFrom(class1))
						throw optError(option, class1 + " is not an Event");
				} else
					methodCall = option.get(2);

				Tree exp;
				try {
					exp = SExpressionParser.parseUTF8(methodCall);
				} catch (TreeParserException e) {
					throw optError(option, "Invalid S-Expression", e);
				}

				publisher.addSubscribeMethodCall(object,
						(Class<? extends Event>) class1, filter, exp);

				continue;
			}

			if (opt0.equals("addDevice")) {
				if (option.size() != 4 && option.size() != 5)
					throw optError(option,
							"addDevice option must have 3 or 4 arguments CLASS ID METHOD_EXPRESSION [TYPE]");

				if (object == null)
					throw optError(option, "object must be specified first");

				Class<?> class1 = null;
				try {
					class1 = Class.forName(option.get(1));
				} catch (ClassNotFoundException e) {
					throw optError(option,
							"class not found: \"" + option.get(1) + "\"", e);
				}
				if (!class1.isInterface())
					throw optError(option, class1 + " is not an Interface");

				String deviceID = option.get(2);
				String method = option.get(3);
				PublishType type = null;
				if (option.size() == 5) {
					type = PublishType.valueOf(option.get(4));
					if (type != PublishType.SOCKET
							&& type != PublishType.TUPLESPACE)
						throw optError(option,
								"type must be either SOCKET or TUPLESPACE, but was "
										+ type);
				}

				Tree exp;
				try {
					exp = SExpressionParser.parseUTF8(method);
				} catch (TreeParserException e) {
					throw optError(option, "Invalid S-Expression", e);
				}

				publisher.addDevice(new AddDeviceInfo(object, class1, deviceID,
						exp, type, true));
				continue;
			}

			if (opt0.equals("removeDevice")) {
				if (option.size() != 3 && option.size() != 4)
					throw optError(option,
							"removeDevice option must have 2 or 3 arguments ID METHOD_EXPR [TYPE]");

				if (object == null)
					throw optError(option, "object must be specified first");

				String deviceID = option.get(1);
				String method = option.get(2);
				PublishType type = null;
				if (option.size() == 4) {
					type = PublishType.valueOf(option.get(3));
					if (type != PublishType.SOCKET
							&& type != PublishType.TUPLESPACE)
						throw optError(option,
								"type must be either SOCKET or TUPLESPACE, but was "
										+ type);
				}

				Tree exp;
				try {
					exp = SExpressionParser.parseUTF8(method);
				} catch (TreeParserException e) {
					throw optError(option, "Invalid S-Expression", e);
				}

				publisher.addDevice(new AddDeviceInfo(object, null, deviceID,
						exp, type, false));
				continue;
			}

			// ### GUI

			if (opt0.equals("gui")) {
				if (option.size() != 1)
					throw optError(option, "gui option must have no arguments");

				if (consoleWrapper != null)
					throw optError(option, "gui option can be used only once");

				title = "GenericPublisher" + title;
				consoleWrapper = publisher.initConsoleWrapper(title);

				continue;
			}

			if (opt0.equals("gui-capacity")) {
				if (option.size() != 2)
					throw optError(option,
							"gui-capacity option must have one argument KBYTES");

				if (consoleWrapper == null) {
					throw optError(option, "gui must be activated first");
				}

				int capacity = 0;
				try {
					capacity = Integer.parseInt(option.get(1));
				} catch (NumberFormatException e) {
					throw optError(option, "Please specify a valid number", e);
				}

				consoleWrapper.setCapacity(capacity * 1024);

				continue;
			}

			if (opt0.equals("gui-title")) {
				if (option.size() != 2)
					throw optError(option,
							"gui-title option must have one argument STRING");

				if (consoleWrapper == null) {
					throw optError(option, "gui must be activated first");
				}

				consoleWrapper.setTitle(option.get(1));
				continue;
			}

			if (opt0.equals("panel")) {
				if (option.size() != 3 && option.size() != 4)
					throw optError(option,
							"panel option must have two or three arguments TITLE [CLASS] SEXPR");

				if (consoleWrapper == null)
					throw optError(option, "gui must be activated first");

				String panelTitle = option.get(1);
				Class<?> clazz = Object.class;

				int off = 0;
				if (option.size() == 4) {
					off++;
					try {
						clazz = Class.forName(option.get(2));
					} catch (ClassNotFoundException e) {
						throw optError(option, "", e);
					}
				}

				String expr = option.get(off + 2);
				Tree panelExpr = null;
				try {
					panelExpr = SExpressionParser.parseUTF8(expr);
				} catch (TreeParserException e) {
					throw optError(
							option,
							expr
									+ " is not a valid S-Expression in the --panel option "
									+ option + "");
				}

				Object o = null;
				try {
					synchronized (publisher.idmap) {
						o = panelExpr.getAs(clazz, publisher.idmap);
					}
				} catch (DeserializableException e) {
					throw optError(option, "Can't deserialize component "
							+ panelExpr, e);
				}

				if (!(o instanceof Component))
					throw optError(option, "object is not a component");

				Component comp = (Component) o;

				ConsoleWrapper.addComponent(panelTitle, comp);
				continue;
			}

			if (opt0.equals("control")) {
				if (option.size() != 2)
					throw optError(option,
							"control option must have one argument");

				if (object == null) {
					throw optError(option, "object must be specified first");
				}

				if (consoleWrapper == null)
					throw optError(option, "gui must be activated first");

				String name = option.get(1);

				try {
					Class<?> compClass = Class.forName(name);
					if (!DeviceControlComponent.class
							.isAssignableFrom(compClass))
						throw optError(option, "Class " + name + " is not a "
								+ DeviceControlComponent.class.getName());

					@SuppressWarnings("rawtypes")
					DeviceControlComponent comp = (DeviceControlComponent) compClass
							.getConstructor().newInstance();

					if (!comp.isApplicableForControlling(object.getClass()))
						throw optError(option,
								"Control component " + compClass.getName()
										+ " is not applicable for "
										+ object.getClass().getName());

					@SuppressWarnings({ "rawtypes" })
					StaticDevice device = new StaticDevice(object, id,
							publisher.properties.get(id));
					comp.initialize(object.getClass(), device,
							GenericPublisherMain.class.getClassLoader());

					// add each component as an observer of every component
					for (GUIComponent<?> c : guiComponents) {
						c.addObserver(comp);
						comp.addObserver(c);
					}

					guiComponents.add(comp);

					ConsoleWrapper.addComponent(comp.getDisplayName("eng")
							+ " - " + id, comp.getControlPanel());
				} catch (NoSuchMethodException e) {
					throw optError(option, "Given class " + name
							+ " has no empty constructor.", e);
				} catch (IllegalArgumentException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (SecurityException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (InstantiationException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (IllegalAccessException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (InvocationTargetException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (ClassNotFoundException e) {
					throw optError(option, "Can't invoke constructor", e);
				}

				continue;
			}

			if (opt0.equals("representation")) {
				if (option.size() != 2)
					throw optError(option,
							"representation option must have one argument");

				if (object == null) {
					throw optError(option, "object must be specified first");
				}

				if (consoleWrapper == null)
					throw optError(option, "gui must be activated first");

				String name = option.get(1);

				try {
					Class<?> repClass = Class.forName(name);
					if (!DeviceRepresentationComponent.class
							.isAssignableFrom(repClass))
						throw optError(option, "Class " + name + " is not a "
								+ DeviceRepresentationComponent.class.getName());

					@SuppressWarnings("rawtypes")
					DeviceRepresentationComponent comp = (DeviceRepresentationComponent) repClass
							.getConstructor().newInstance();

					if (!comp.isApplicableForRepresenting(object.getClass()))
						throw optError(option,
								"Control component " + repClass.getName()
										+ " is not applicable for "
										+ object.getClass().getName());

					@SuppressWarnings({ "rawtypes" })
					StaticDevice device = new StaticDevice(object, id,
							publisher.properties.get(id));
					comp.initialize(object.getClass(), device,
							GenericPublisherMain.class.getClassLoader());

					// add each component as an observer of every component
					for (GUIComponent<?> c : guiComponents) {
						c.addObserver(comp);
						comp.addObserver(c);
					}

					guiComponents.add(comp);

					ConsoleWrapper.addComponent(comp.getDisplayName("eng")
							+ " - " + id, comp.getRepresentationPanel());
				} catch (NoSuchMethodException e) {
					throw optError(option, "Given class " + name
							+ " has no empty constructor.", e);
				} catch (IllegalArgumentException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (SecurityException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (InstantiationException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (IllegalAccessException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (InvocationTargetException e) {
					throw optError(option, "Can't invoke constructor", e);
				} catch (ClassNotFoundException e) {
					throw optError(option, "Can't invoke constructor", e);
				}
				continue;
			}

			// ### DEVICE INFO

			if (opt0.equals("position")) {
				if (option.size() != 2)
					throw optError(option,
							"position option must one argument POSITION_MAP");

				if (object == null)
					throw optError(option, "object must be specified first");

				Tree posExp = null;
				try {
					posExp = SExpressionParser.parseUTF8(option.get(1));
				} catch (TreeParserException e) {
					throw optError(option, option.get(1)
							+ " is not a valid position", e);
				}

				if (!writeDeviceInfo)
					continue;

				GParameterized clazz = new GParameterized(Map.class,
						new GClass[] { GClass.fromClass(String.class),
								GClass.fromClass(Position.class) });

				try {
					writeInformation(id, "position", posExp.getAs(clazz));
				} catch (DeserializableException e) {
					throw optError(option, "Can't deserialize information "
							+ posExp, e);
				}
				continue;
			}

			if (opt0.equals("name")) {
				if (option.size() != 2)
					throw optError(option,
							"name option must have one argument NAME_MAP");

				if (object == null)
					throw optError(option, "object must be specified first");

				Tree nameExp = null;
				try {
					nameExp = SExpressionParser.parseUTF8(option.get(1));
				} catch (TreeParserException e) {
					throw optError(option, "Invalid S-Expression", e);
				}

				if (!writeDeviceInfo)
					continue;

				GParameterized clazz = new GParameterized(Map.class,
						new GClass[] { GClass.fromClass(String.class),
								GClass.fromClass(String.class) });

				try {
					writeInformation(id, "displaynames", nameExp.getAs(clazz));
				} catch (DeserializableException e) {
					throw optError(option, "Can't deserialize information "
							+ nameExp, e);
				}

				continue;
			}

			if (opt0.equals("property")) {
				if (option.size() != 3)
					throw optError(option,
							"property option must have two arguments");

				if (object == null)
					throw optError(option, "object must be specified first");

				String key = option.get(1);
				try {
					Tree valueExp = SExpressionParser.parseUTF8(option.get(2));

					if (!writeDeviceInfo)
						continue;

					try {
						writeInformation(id, key, valueExp.getAs(Object.class));
					} catch (DeserializableException e) {
						throw optError(option, "Value can't be deserialized", e);
					}
				} catch (TreeParserException e) {
					throw optError(option, " invalid S-Expression", e);
				}
				continue;
			}

			// ### MISC

			if (opt0.equals("in")) {
				if (option.size() != 2)
					throw optError(option, "in option must have one argument");
				// immediately change the streams
				InputStream is = null;
				try {
					is = new FileInputStream(option.get(1));
				} catch (FileNotFoundException e) {
					throw optError(option, "", e);
				}
				System.setIn(is);
				continue;
			}

			if (opt0.equals("out")) {
				if (option.size() != 2)
					throw optError(option, "out option must have one argument");
				// immediately change the streams
				PrintStream ps = null;
				try {
					ps = new PrintStream(option.get(1));
				} catch (FileNotFoundException e) {
					throw optError(option, "", e);
				}
				System.setOut(ps);
				continue;
			}

			if (opt0.equals("err")) {
				if (option.size() != 2)
					throw optError(option, "err option must have one argument");
				// immediately change the streams
				PrintStream ps = null;
				try {
					ps = new PrintStream(option.get(1));
				} catch (FileNotFoundException e) {
					throw optError(option, "", e);
				}
				System.setErr(ps);
				continue;
			}

			if (opt0.equals("log-level")) {
				if (option.size() != 2 && option.size() != 3)
					throw optError(option,
							"log-level option must have one or two arguments: [CATEGORY] LEVEL");

				if (option.size() == 2) {
					Logger logger = Logger.getRootLogger();
					logger.setLevel(Level.toLevel(option.get(1)));
				} else {
					Logger logger = Logger.getLogger(option.get(1));
					logger.setLevel(Level.toLevel(option.get(2)));
				}

				continue;
			}

			throw new Exception("Unknown option: " + option);
		}

		/*
		 * END OF PARSE COMMAND LINE ARGUMENTS
		 * ###################################################################
		 */

		Runnable shutDown = new Runnable() {
			private AtomicBoolean done = new AtomicBoolean(false);

			@Override
			public void run() {
				// only run the first time
				// may be called as response to "q" and as ShutDown hook
				if (done.getAndSet(true))
					return;

				publisher.shutDown();

				for (Pair<Object, String> p : objects) {
					Object o = p.getFirst();

					// first run shutdown-methods, then the quit-Method
					LinkedList<Tree> methods = shutdownMethods.get(o);
					if (methods != null) {
						ObjectDescriptor descr = new ObjectDescriptor(o);
						HashMap<String, Object> map = new HashMap<String, Object>();
						for (Tree m : methods) {
							try {
								SerializerHelper.invokeObjectMethod(descr, m, o
										.getClass().getClassLoader(), map);
							} catch (MethodInvocationException e) {
								e.printStackTrace();
							} catch (NoMatchingMethodFoundException e) {
								e.printStackTrace();
							}
						}
					}

					if (!(o instanceof Quittable))
						continue;

					try {
						((Quittable) o).quit();
					} catch (java.lang.Exception e) {
						System.err.println("Exception during call of quit ");
						e.printStackTrace();
					}
				}
			}
		};

		Runtime.getRuntime().addShutdownHook(new Thread(shutDown));

		if (!control)
			return;

		System.out.println("Enter i to show various information.");
		System.out.println("Press q to quit");
		// wait for user to type "q" to quit the server
		BufferedReader inputReader = new BufferedReader(new InputStreamReader(
				System.in));
		while (true) {
			String command = "";

			try {
				command = inputReader.readLine();
			} catch (IOException e) {
				throw new Exception(e);
			}
			if (command == null)
				break;

			if (command.equals("i"))
				printInfo(publisher);
			else if (command.equals("q")) {
				break;
			} else {
				System.out.println("Unknown command: " + command);
				System.out.println("use q to stop, i to show info");
			}
		}

		shutDown.run();

		System.out.println("Stopped");
	}

	private static void publishPOJOP(final GenericPublisher publisher,
			final int p, final Object o, final String id_,
			final ServerType type, final boolean useInfrastructure,
			final boolean implicit) {
		new Thread(new Runnable() {
			@Override
			public void run() {
				POJOPPublishHandle handle;
				try {
					handle = publisher.publishPOJOP(o, id_, p, type,
							useInfrastructure, implicit);
				} catch (Exception e) {
					e.printStackTrace();
					return;
				}
				System.out.println(handle.prettyPrint());
			}
		}, "Publish " + id_ + " " + type + " " + p).start();
	}

	private static void writeInformation(final String id, final String key,
			final Object value) {
		publisher.writeInformation(id, key, value);
	}
}
